/* hl2demo parser */

/* Copyright (c) 2007-2012, ben "boin" bonoeil

   Redistribution and use in source and binary forms, with or without
   modification, are permitted provided that the following conditions
   are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above
      copyright notice, this list of conditions and the following
      disclaimer in the documentation and/or other materials provided
      with the distribution.
      * Neither the name of the ben "boin" bonoeil nor the names of its
      contributors may be used to endorse or promote products derived
      from this software without specific prior written permission.

    NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE
    GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY
    ben "boin" bonoeil ''AS IS'' AND ANY EXPRESS OR IMPLIED
    WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
    OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED. IN NO EVENT SHALL ben "boin" bonoeil BE LIABLE FOR ANY
    DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
    GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
    WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
    NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include "hl2demo_parser.h"
#include "hl2demo_print.h"
#include "hl2demo_buffer.h"
#include "hl2demo_io.h"
#include "hl2demo_ieee754.h"

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>

static hl2demo_parser_cb_t hl2demo_parser_fct = 0;
static void * hl2demo_parser_dat = 0;

typedef struct {
  FILE * fp;                    /**< File pointer.         */
  size_t pos;                   /**< Position in file.     */
  char * buf;                   /**< Data buffer.          */
  size_t len;                   /**< Length of data block. */
} filedata_t;

static const char dem_magic[8] = HL2DEMO_MAGIC;

static int peek32(const void * const mem) {
  const uint8_t * const ptr = mem;
  return (int)(int32_t)(ptr[0] + (ptr[1]<<8) + (ptr[2]<<16) + (ptr[3]<<24));
}

static void poke32( void * const mem, int v) {
  uint8_t * const ptr = mem;
  ptr[0] = v;  ptr[1] = v>>8;  ptr[2] = v>>16;  ptr[3] = v>>24;
}

static int offset(const void * addr, const void * org) {
  return (const char *)addr - (const char *) org;
}

static const char * demmsg_str(int msg) {
  switch (msg) {
  case dem_signon:       return "sign-on";
  case dem_packet:       return "packet";
  case dem_synctick:     return "synctick";
  case dem_consolecmd:   return "concmd";
  case dem_usercmd:      return "usercmd";
  case dem_datatables:   return "data-tables";
  case dem_stop:         return "stop";
  case dem_stringtables: return "stringtables";
  }
  return "undef";
}

static const char * fdemo_flagstr[8] = {
  "NONE",                       /* 000 */
  "ORG2"                        /* 001 */
  "ANG2"                        /* 010 */
  "ORG2,ANG2"                   /* 011 */
  "!INT"                        /* 100 */
  "ORG2,!INT"                   /* 101 */
  "ANG2,!INT"                   /* 110 */
  "ORG2,ANG2,!INT"              /* 111 */
};

static int check_zero(char * s, int max) {
  int i;
  for (i=0; i<max-1; i++) if (!s[i]) return 0;
  s[i] = 0;
  return -1;
}

static const char thex[] = "0123456789ABCDEF";

static const char * hexdump(const void * mem, int n, int offset) {
  static char buf[16*(3+1)+16+32];
  int i,j,k;
  const uint8_t * const ptr = (const uint8_t *)mem;

  if (n>16) n = 16;

  sprintf(buf,"%07X ",offset);

  for (i=0, j=8, k=j+16*3; i<n; ++i) {
    int v    = ptr[i];
    buf[j++] = thex[v>>4];    buf[j++] = thex[v&15];    buf[j++] = ' ';
    buf[k++] = isgraph(v) ? v : '.';
  }
  for (; i<16; ++i) {
    buf[j++] = '-';    buf[j++] = '-';    buf[j++] = i == 15 ? '|' : ' ';
  }
  buf[k] = 0;
  return buf;
}

/* ---------------------------------------------------------------------- */

static char errstr[1024];

const char * hl2demo_get_error(void)
{
  errstr[sizeof(errstr)-1] = 0;
  return errstr;
}

void hl2demo_set_error(const char * fmt, ...)
{
  va_list list;
  va_start(list,fmt);
  debug_va(fmt, list);
  vsnprintf(errstr,sizeof(errstr),fmt,list);
  va_end(list);
}

#define set_error hl2demo_set_error

static void sys_error(const char * msg)
{
  set_error("%s (%s)\n", msg, strerror(errno));
}

/* ---------------------------------------------------------------------- */
static void * xxxmalloc(unsigned long size, const char * what)
{
  void * buf = malloc(size);
  if (!buf) {
    set_error("can't alloc <%s> %d bytes (%s)\n",
              what?what:"buffer", size, strerror(errno));
  }
  return buf;
}

static int
xxxread_data(FILE *inp, void * buf, int size, const char * what)
{
  int err = read_data(inp, buf, size);
  if (err) {
    set_error("can't read <%s> %d bytes (%s)\n",
              what?what:"buffer",size, strerror(errno));
  }
  return err;
}

static void debug_dem_header(const hl2demo_header_t * hd)
{
  debug("-------- HEADER\n");
  debug("%08X magic   : %7s\n",offset(hd->magic,hd),hd->magic);
  debug("%08X dem ver : %d\n",offset(&hd->dem_ver,hd),hd->dem_ver);
  debug("%08X net ver : %d\n",offset(&hd->net_ver,hd),hd->net_ver);
  debug("%08X tv-name : %s\n",offset(hd->tv_name,hd),hd->tv_name);
  debug("%08X sv-name : %s\n",offset(hd->sv_name,hd),hd->sv_name);
  debug("%08X mp-name : %s\n",offset(hd->mp_name,hd),hd->mp_name);
  debug("%08X gm-name : %s\n",offset(hd->gm_name,hd),hd->gm_name);
  debug("%08X pb time : %f\n",offset(&hd->pb_time,hd),hd->pb_time);
  debug("%08X pb tick : %-10d tps : %.2f\n",
        offset(&hd->pb_tick,hd),hd->pb_tick,(float)hd->pb_tick/hd->pb_time);
  debug("%08X pb fram : %-10d fps : %.2f\n",
        offset(&hd->pb_fram,hd),hd->pb_fram,(float)hd->pb_fram/hd->pb_time);
  debug("%08X sig len : %d\n",offset(&hd->sig_len,hd),hd->sig_len);
  debug("%08X total   : %d\n",offset(hd+1,hd),sizeof(*hd));
}


static void debug_data_block(const void * buf, int len, int pos)
{
  for (const int8_t * mem = buf; len > 0; len -= 16, pos += 16, mem += 16) {
    const char * s = hexdump(mem, len, pos);
    debug("%s\n",s);
  }
}


int hl2demo_header_read(hl2demo_header_t * hd, FILE * inp)
{
  int err;
  struct hl2demo_fheader_s fhd; /* file header */
  hl2demo_header_t dummy;

  debug("hl2demo_header_read(%p,%p)\n",hd,inp);

  if (!hd) hd = &dummy;

  err = xxxread_data(inp, &fhd, sizeof(fhd),"header");
  if (!err) {
    memcpy(hd->magic, fhd.magic, sizeof(hd->magic));
    hd->dem_ver = peek32(fhd.dem_ver);
    hd->net_ver = peek32(fhd.net_ver);
    memcpy(hd->sv_name, fhd.sv_name, sizeof(hd->sv_name));
    memcpy(hd->tv_name, fhd.tv_name, sizeof(hd->tv_name));
    memcpy(hd->mp_name, fhd.mp_name, sizeof(hd->mp_name));
    memcpy(hd->gm_name, fhd.gm_name, sizeof(hd->gm_name));
    hd->pb_time = ieee754_get32(peek32(fhd.pb_time));
    hd->pb_tick = peek32(fhd.pb_tick);
    hd->pb_fram = peek32(fhd.pb_fram);
    hd->sig_len = peek32(fhd.sig_len);

    err = -1;
    if (memcmp(dem_magic, hd->magic, sizeof(dem_magic))) {
      hl2demo_set_error("not an hl2demo file; missing signature\n");
    } else {
      err = 0;
    }

    /* Should these trigger an error ? */
    if (hd->dem_ver != DEM_PROTOCOL) {
      /* hl2demo_set_error */
      warning ("wrong protocol version found %d instead of %d\n",
               hd->dem_ver,DEM_PROTOCOL);
    }
    if (hd->net_ver != NET_PROTOCOL) {
      /* hl2demo_set_error */
      warning ("wrong protocol version found %d instead of %d\n",
               hd->net_ver,NET_PROTOCOL);
    }

    /* Sanity checks */
    if (check_zero(hd->sv_name,sizeof(hd->sv_name))) {
      warning("server name is not zero terminated: truncated\n");
    }
    if (check_zero(hd->tv_name,sizeof(hd->tv_name))) {
      warning("tv name is not zero terminated: truncated\n");
    }
    if (check_zero(hd->mp_name,sizeof(hd->mp_name))) {
      warning("map name is not zero terminated: truncated\n");
    }
    if (check_zero(hd->gm_name,sizeof(hd->gm_name))) {
      warning("game name is not zero terminated: truncated\n");
    }
    if (!hd->pb_time || !hd->pb_tick || !hd->pb_fram || !hd->sig_len) {
      warning("Demo file has not been properly stopped by recorder\n");
    }

    debug_dem_header(hd);
  }

  debug("hl2demo_header_read() => %d\n",err);

  return err;
}

static int
read_cmdheader(FILE * inp,  uint8_t * cmd, int32_t * tick)
{
  uint8_t c;
  int err, t;
  err = read_byte(inp,&c);
  if (err) {
    error("cmdheader: can't read command byte\n");
  } else if (c < 1 || c >= dem_lastcmd) {
    err = -1;
    error("cmdheader: cmd %d out of rang [1..%d]\n",
          c, dem_lastcmd-1);
  } else if (err = read_int32(inp,&t), err) {
    if (c != dem_stop) {
      error("cmdheader: can't read tick (cmd %d)\n",c);
    } else {
      err = 0;
      t = 0;
    }
  }
  if (cmd) *cmd = err ? dem_stop : c;
  if (tick) *tick = t;
  return err;
}

static int
read_rawdata(FILE * inp, filedata_t * filedata, int skip)
{
  int32_t size;
  int err;

  err = read_int32(inp,&size);
  if (err) {
    sys_error("can't read raw-data size");
  } else {
    debug("raw-data size is %d bytes\n", size);

    if (!filedata) {
      skip = 1;
    } else {
      filedata->fp  = inp;
      filedata->pos = ftell(inp);
      filedata->len = size;
    }

    if (size > 0x00FFFFFF) {
      warning("size of raw-data (@%d) %d>16MB\n", filedata->pos, size);
    }
    if (size > 0x0FFFFFFF) {
      set_error("size of raw-data (@%d) %d>64MB\n", filedata->pos, size);
      err = -1;
    } else if (skip) {
      err = fseek(inp, size, SEEK_CUR);
    } else {
      filedata->buf = xxxmalloc(size,"raw-data block");
      err = -!filedata->buf;
      err = err
        ? err : xxxread_data(inp, filedata->buf, size,"raw-data block");
    }
  }

  return -!!err;
}

static int
read_seqinfo(FILE * inp, int32_t * seq_in, int32_t * seq_out)
{
  int err = - (read_int32(inp, seq_in) || read_int32(inp, seq_out));
  if (err) sys_error("seq-info");
  return err;
}

static int
read_cmdinfo(FILE * inp, hl2demo_cmdinfo_t * cmdinfo)
{
  const int size = sizeof(*cmdinfo);
  int err = -(size != fread(cmdinfo,1,size,inp));
  if (err) sys_error("seq-info");
  return err;
}

static int
read_concmd(FILE * inp, filedata_t * filedata, int skip)
{
  return read_rawdata(inp, filedata, skip);
}

static int
read_netdatatables(FILE * inp, filedata_t * filedata, int skip)
{
  return read_rawdata(inp, filedata, skip);
}

static int
read_stringtables(FILE * inp, filedata_t * filedata, int skip)
{
  return read_rawdata(inp, filedata, skip);
}

static int
read_usercmd(FILE * inp, int * seq, filedata_t * filedata, int skip)
{
  int err, sequence = -1;

  err = read_int32(inp, &sequence);
  if (!err) {
    err = read_rawdata(inp, filedata, skip);
  }
  if (seq) *seq = sequence;

  return err;
}



int hl2demo_parser(hl2demo_t * dm, FILE * inp, int skip)
{
  /* hl2demo_cmdinfo_t info; */

  int err = 0, usercmd;
  int cnt;

  if (!dm || !inp) {
    err = -1;
    set_error("null pointer argument(s)\n");
  }

  memset(dm,0,sizeof(*dm));

  for (cnt=0; !err; ++cnt) {
    int tck, hasinfo;
    uint8_t cmd;
    size_t pos;
    hl2demo_message_t * msg, tmsg;

    filedata_t filedata;
    memset(&filedata,0,sizeof(filedata));

    msg = 0;
    hasinfo = 0;
    pos = ftell(inp);

    err = read_cmdheader(inp, &cmd, &tck);
    if (err) {
      set_error("file corrupt; reading command header #%u at %u (%s)\n",
                (int) cnt, (int) pos,
                errno
                ? strerror(errno)
                : ( feof(inp) ? "EOF" : "???"));
      break;
    }

    debug("POS:%08x MSG:%06u TCK:%06u CMD:%d (%s)\n",
          (int)pos, cnt, tck, (int)cmd, demmsg_str(cmd));

    switch (cmd) {
    case dem_synctick:
    case dem_stop:
      break;
    case dem_consolecmd:
      err = read_concmd(inp, &filedata, skip&PARSER_SKIP_DATA);
      if (!err && !(skip&PARSER_SKIP_DATA)) {
        debug_data_block(filedata.buf, filedata.len, filedata.pos);
      }
      break;
    case dem_datatables:
      err = read_netdatatables(inp, &filedata, skip&PARSER_SKIP_DATA);
      if (!err && !(skip&PARSER_SKIP_DATA)) {
        debug_data_block(filedata.buf, filedata.len, filedata.pos);
      }
      break;
    case dem_usercmd:
      err = read_usercmd(inp, &usercmd, &filedata, skip&PARSER_SKIP_DATA);
      if (!err) {
        debug ("SEQ:%d\n",usercmd);
        if (!(skip&PARSER_SKIP_DATA)) {
          debug_data_block(filedata.buf, filedata.len, filedata.pos);
        }
      }
      break;
    case dem_signon:
    case dem_packet:
      hasinfo = 1;
      break;

    case dem_stringtables:
      err = read_stringtables(inp, &filedata, skip&PARSER_SKIP_DATA);
      if (!err && !(skip&PARSER_SKIP_DATA)) {
        debug_data_block(filedata.buf, filedata.len, filedata.pos);
      }
      break;
    /*   { */
    /*   char tmp[256]; */
    /*   size_t rpos = ftell(inp); */
    /*   debug ("stringtables\n",cmd); */

    /*   err = xxxread_data(inp, tmp, sizeof(tmp), "string tables") ; */
    /*   if (!err) { */
    /*  debug_data_block(tmp, sizeof(tmp), rpos); */
    /*   } */
    /*   if (fseek(inp,rpos,SEEK_SET) == -1) { */
    /*  err = -1; */
    /*  set_error("seek error (%s)", strerror(errno)); */
    /*   } */
    /* } break; */

    default:

      set_error("unknown demo command <%d>\n", cmd);
      err = -1;
      break;
    }

    /* $$$ should process dem_stop to the end of the loop to store
     * this last message */
    if (err || cmd == dem_stop) {
      break;
    }

    if (skip != PARSER_SKIP_ALL) {
      msg = xxxmalloc(sizeof(*msg), "message");
      if (!msg) {
        err = -1;
        break;
      }
    } else {
      msg = &tmsg;
    }

    memset(msg,0,sizeof(*msg));
    msg->cnt = cnt;
    msg->tck = tck;
    msg->cmd = cmd;

    if (hasinfo) {
      hl2demo_cmdinfo_t cmdinfo;
      int32_t seq_in, seq_out;

      memset(&filedata,0,sizeof(filedata));

      /* Command Info */
      err = read_cmdinfo(inp, &cmdinfo);
      if (err) {
        break;
      }
      if (msg) {
        msg->cmdinfo = cmdinfo;
      }
      if (cmdinfo.flags != (cmdinfo.flags&7)) {
        warning("unknown flags in command (%d); ignored\n", cmdinfo.flags);
      }
      if (cmdinfo.flags != FDEMO_NORMAL) {
        hl2demo_vector_t * vo1  = &cmdinfo.vo1,  * vo2  = &cmdinfo.vo2;
        hl2demo_qangle_t * va1  = &cmdinfo.va1,  * va2  = &cmdinfo.va2;
        hl2demo_qangle_t * lva1 = &cmdinfo.lva1, * lva2 = &cmdinfo.lva2;
        debug("flag : %d [%s]\n"
              "vo1  : [%8.2f %8.2f %8.2f]  " "vo2  : [%8.2f %8.2f %8.2f]\n"
              "va1  : [%8.2f %8.2f %8.2f]  " "va2  : [%8.2f %8.2f %8.2f]\n"
              "lva1 : [%8.2f %8.2f %8.2f]  " "lva2 : [%8.2f %8.2f %8.2f]\n",
              cmdinfo.flags,fdemo_flagstr[cmdinfo.flags&7],
              vo1->x,  vo1->y,  vo1->z,  vo2->x,  vo2->y,  vo2->z,
              va1->x,  va1->y,  va1->z,  va2->x,  va2->y,  va2->z,
              lva1->x, lva1->y, lva1->z, lva2->x, lva2->y, lva2->z
              );
      }

      /* Sequence Info */
      err = read_seqinfo(inp, &seq_in, &seq_out);
      if (err) {
        break;
      }
      if (msg) {
        msg->seq_in  = seq_in;
        msg->seq_out = seq_out;
      }
      debug("SEQ:%d,%d\n", seq_in, seq_out);

      /* Command Data */
      err = read_rawdata(inp, &filedata, skip&PARSER_SKIP_DATA);
      if (err) {
        break;
      }
      debug("LEN:%d POS:%d\n", filedata.len, filedata.pos);

      if (msg) {
        msg->pos = filedata.pos;
        msg->buf = filedata.buf;
        msg->len = filedata.len;
      }

      if (!(skip&PARSER_SKIP_DATA)) {
        debug_data_block(filedata.buf, filedata.len, filedata.pos);
      }

      /* TEST */
      /* if (0 && filedata.buf) {
        int len = filedata.len, nb=0;
        uint8_t * ptr = (uint8_t *) filedata.buf;

        if (len < 3) {
          debug("block too small: %d\n",len);
          break;
        }

        while (len != 0) {
          int blen;
          ptr+=2; len -= 3;
          blen = *ptr++;
          if (len < blen) {
            debug("not enougth data for block len (%d<%d)\n",len,blen);
            break;
          }
          ptr += blen;
          len -= blen;
          ++nb;
        }
        if (len == 0) {
          debug("Good block (%d sub-commands)\n",nb);
        }

        }*/
    }

    if (msg != &tmsg) {
      if (!dm->msgtail) {
        dm->msgtail = dm->msghead = msg;
      } else {
        dm->msgtail->nxt = msg;
        dm->msgtail = msg;
      }
    }

    if ( hl2demo_parser_fct ) {
      err = hl2demo_parser_fct(msg, hl2demo_parser_dat);
    }
  }

  return err;
}


void hl2demo_parser_callback(hl2demo_parser_cb_t fct, void * dat)
{
  hl2demo_parser_fct = fct;
  hl2demo_parser_dat = dat;
}
